fix: update coverage.include and coverage.exclude#9426
fix: update coverage.include and coverage.exclude#9426DevJoaoLopes wants to merge 11 commits intovitest-dev:mainfrom
Conversation
✅ Deploy Preview for vitest-dev ready!Built without sensitive environment variables
To edit notification comments on pull requests, go to your Netlify project configuration. |
@AriPerkkio Sure, I apologize for not doing it from the beginning... I'll work on it. 😅 |
| return false | ||
| } | ||
| for (const projectRoot of roots) { | ||
| const relativePath = slash(relative(projectRoot, filename)) |
There was a problem hiding this comment.
Doesn't this actually change how defined globs are interpreted when using --project filter?
- When it's not used, globs are relative to
root✅ - When it's used, globs are relative to project's
root❌
├── package.json
├── vitest.config.ts
└── packages
├── one
│ ├── src
│ │ └── math.ts
│ └── test
│ └── math.test.ts
└── two
├── src
│ └── get-user.ts
└── test
└── get-user.test.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
include: ["./packages/**/*.ts"],
},
projects: [
{
test: {
name: "One",
root: "./packages/one",
},
},
{
test: {
name: "Two",
root: "./packages/two",
},
},
],
},
});> vitest --run --coverage
RUN v4.0.16 /x/examples/packages/vitest-example
Coverage enabled with v8
✓ Two test/get-user.test.ts (1 test) 1ms
✓ One test/math.test.ts (1 test) 1ms
Test Files 2 passed (2)
Tests 2 passed (2)
Start at 10:12:31
Duration 115ms (transform 32ms, setup 0ms, import 43ms, tests 2ms, environment 0ms)
% Coverage report from v8
--------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------|---------|----------|---------|---------|-------------------
All files | 40 | 100 | 40 | 40 |
one/src | 25 | 100 | 25 | 25 |
math.ts | 25 | 100 | 25 | 25 | 6-14
two/src | 100 | 100 | 100 | 100 |
get-user.ts | 100 | 100 | 100 | 100 |
--------------|---------|----------|---------|---------|-------------------
This should show coverage for packages/one, but it's missing:
> vitest --run --coverage --project one
RUN v4.0.16 /x/examples/packages/vitest-example
Coverage enabled with v8
✓ One test/math.test.ts (1 test) 1ms
✓ sum 0ms
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 10:12:50
Duration 99ms (transform 14ms, setup 0ms, import 19ms, tests 1ms, environment 0ms)
% Coverage report from v8
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 0 | 0 | 0 | 0 |
----------|---------|----------|---------|---------|-------------------
There was a problem hiding this comment.
Okay, I will evaluate these cases.
packages/vitest/src/node/coverage.ts
Outdated
| const matchTarget = isExternal ? filename : relativePath | ||
|
|
||
| this.globCache.set(filename, included) | ||
| if (excludes.length && pm.isMatch(matchTarget, excludes, { dot: true })) { |
There was a problem hiding this comment.
excludes is always defined array.
| if (excludes.length && pm.isMatch(matchTarget, excludes, { dot: true })) { | |
| if (pm.isMatch(matchTarget, excludes, { dot: true })) { |
| continue | ||
| } | ||
|
|
||
| if (pm.isMatch(matchTarget, includeGlobs, { dot: true, contains: true })) { |
There was a problem hiding this comment.
Why do we need 2x pm.isMatch calls, instead of doing just one with passing ignore: options.exclude? Any examples that would fail without 2x calls?
There was a problem hiding this comment.
You’re right that we call pm.isMatch twice, but they serve different purposes and need distinct options:
First call (excludes, { dot: true }): early-out for any path matching coverage.exclude, regardless of how includes are written. This prevents excluded files from slipping through when an include pattern is broad.
Second call (includeGlobs, { dot: true, contains: true }): only after passing exclusion do we check if the file qualifies for coverage.include. The contains: true is important for patterns like "src/utils" that should match nested paths (e.g., src/utils/math.ts), which a strict match would miss.
Dropping either call breaks behavior: without the exclude check, excluded files (e.g., __mocks__) get counted; without the include check (with contains: true), legitimate files under broad includes are skipped.
| include: [normalizeURL(import.meta.url)], | ||
| coverage: { | ||
| reporter: 'json', | ||
| exclude: ['./utils.ts'], | ||
| exclude: ['./utils.ts', '**/test/**'], | ||
| }, | ||
| }) | ||
|
|
There was a problem hiding this comment.
Looks like you needed to add **/test/** here to avoid test/coverage-test/test/bundled-sources.test.ts being showed up here. I think that's actually bug.
Debugging this case further shows that isIncluded is falsely returning true for isIncluded('/x/vitest/test/coverage-test/test/bundled-sources.test.ts') even when this.options.excludes contains following:
[
'./utils.ts',
'/x/vitest/test/coverage-test/test/bundled-sources.test.ts',
'/x/vitest/test/coverage-test/fixtures/configs/vitest.config.ts',
'vitest.config.ts',
'vitest.config.mts',
'vitest.config.cts',
'vitest.config.js',
'vitest.config.mjs',
'vitest.config.cjs',
'vite.config.ts',
'vite.config.mts',
'vite.config.cts',
'vite.config.js',
'vite.config.mjs',
'vite.config.cjs',
'**/virtual:*',
'**/__x00__*',
'**/node_modules/**'
]| include: [normalizeURL(import.meta.url)], | |
| coverage: { | |
| reporter: 'json', | |
| exclude: ['./utils.ts'], | |
| exclude: ['./utils.ts', '**/test/**'], | |
| }, | |
| }) | |
| exclude: ['./utils.ts'], |
There was a problem hiding this comment.
Don't remove include and coverage, just change the coverage.exclude back to exclude: ['./utils.ts'].
dab0aa3 to
c98cb54
Compare

Description
Fixes
coverage.include/coverage.excludeglob matching so it behaves liketest.include/test.exclude(relative to the project root), addressing Vitest v4 regressions reported in #9395.In v4, matching was done against the full (absolute) filename with
picomatchusingcontains: trueandignore, which made patterns such as./*.tsor**/foo/**unexpectedly match parts of the absolute path (including the CWD), causing everything to be excluded.This change:
./*.tswork as expected./.../foo/...), by using relative paths for in-project files and only using the absolute filename for truly external files.allowExternal: falsesemantics by skipping external files instead of incorrectly failing matches for everything.fix #9395
Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
pnpm-lock.yamlunless you introduce a new test example.Tests
pnpm test:ci.Documentation
pnpm run docscommand.Changesets
feat:,fix:,perf:,docs:, orchore:.